/*
 * Copyright (C) 2022 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.google.copybara.util;

import static com.google.common.truth.Truth.assertThat;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Map;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;

@RunWith(JUnit4.class)
public class AutoPatchUtilTest {

  private static final String PATCH_FILE_PREFIX = "This is a patch file generated by Copybara!\n";
  private static final String PATCH_FILE_NAME_SUFFIX = ".patch";
  private static final String SOME_DIR = "some/dir/";
  private static final boolean VERBOSE = true;

  // Command requires the working dir as a File, and Jimfs does not support Path.toFile()
  @Rule public final TemporaryFolder tmpFolder = new TemporaryFolder();
  private Path origin;
  private Path destination;
  private Path root;

  String fileFormat =
      "public static %s() {\n"
          + "  System.out.println(\"%s\");\n"
          + "}\n\n\n\n\n\n\n\n\n\n\n\n\n"
          + "public static common() {"
          + "  System.out.println(\"common\");\n"
          + "}\n\n\n\n\n\n\n\n\n\n\n\n\n"
          + "public static %sAgain() {\n"
          + "  System.out.println(\"%s again\");"
          + "}";

  public Map<String, String> testEnv;

  @Before
  public void setUp() throws IOException {
    Path rootPath = tmpFolder.getRoot().toPath();
    origin = createDir(rootPath, "origin");
    destination = createDir(rootPath, "destination");
    root = createDir(rootPath, "root");

    testEnv = System.getenv();
  }

  @Test
  public void generatedPatchFilesAreReversible() throws Exception {
    writeFile(origin, SOME_DIR.concat("file1.txt"), "foo-origin");
    writeFile(origin, SOME_DIR.concat("file2.txt"), "bar-origin");
    writeFile(
        origin,
        SOME_DIR.concat("file3.java"),
        String.format(fileFormat, "foo", "foo", "foo", "foo"));

    writeFile(
        origin, SOME_DIR.concat("file3.txt"), "foo-origin\n\nsome common content\n\nfoo-origin");
    writeFile(destination, SOME_DIR.concat("file1.txt"), "foo-destination");
    writeFile(destination, SOME_DIR.concat("file2.txt"), "bar-destination");
    writeFile(
        destination,
        SOME_DIR.concat("file3.java"),
        String.format(fileFormat, "bar", "bar", "bar", "bar"));

    AutoPatchUtil.generatePatchFiles(
        origin,
        destination,
        Path.of(SOME_DIR),
        null,
        VERBOSE,
        testEnv,
        PATCH_FILE_PREFIX,
        PATCH_FILE_NAME_SUFFIX,
        root,
        false,
        false,
        Glob.ALL_FILES);
    assertThat(Files.readString(destination.resolve(SOME_DIR.concat("file1.txt"))))
        .isEqualTo("foo-destination");
    assertThat(Files.readString(destination.resolve(SOME_DIR.concat("file2.txt"))))
        .isEqualTo("bar-destination");
    assertThat(Files.readString(destination.resolve(SOME_DIR.concat("file3.java"))))
        .isEqualTo(String.format(fileFormat, "bar", "bar", "bar", "bar"));

    AutoPatchUtil.reversePatchFiles(
        destination, root.resolve(SOME_DIR), PATCH_FILE_NAME_SUFFIX, testEnv);
    assertThat(Files.readString(destination.resolve(SOME_DIR.concat("file1.txt"))))
        .isEqualTo("foo-origin");
    assertThat(Files.readString(destination.resolve(SOME_DIR.concat("file2.txt"))))
        .isEqualTo("bar-origin");
    assertThat(Files.readString(destination.resolve(SOME_DIR.concat("file3.java"))))
        .isEqualTo(String.format(fileFormat, "foo", "foo", "foo", "foo"));
  }

  @Test
  public void patchFilesGeneratedAndWritten() throws Exception {
    writeFile(origin, SOME_DIR.concat("file1.txt"), "foo-origin");
    writeFile(origin, SOME_DIR.concat("file2.txt"), "bar-origin");
    writeFile(
        origin,
        SOME_DIR.concat("file3.java"),
        String.format(fileFormat, "foo", "foo", "foo", "foo"));

    writeFile(
        origin, SOME_DIR.concat("file3.txt"), "foo-origin\n\nsome common content\n\nfoo-origin");
    writeFile(destination, SOME_DIR.concat("file1.txt"), "foo-destination");
    writeFile(destination, SOME_DIR.concat("file2.txt"), "bar-destination");
    writeFile(
        destination,
        SOME_DIR.concat("file3.java"),
        String.format(fileFormat, "bar", "bar", "bar", "bar"));

    AutoPatchUtil.generatePatchFiles(
        origin,
        destination,
        Path.of(SOME_DIR),
        null,
        VERBOSE,
        testEnv,
        PATCH_FILE_PREFIX,
        PATCH_FILE_NAME_SUFFIX,
        root,
        true,
        true,
        Glob.ALL_FILES);

    assertThat(
            Files.readString(
                root.resolve(SOME_DIR).resolve("file1.txt".concat(PATCH_FILE_NAME_SUFFIX))))
        .isEqualTo(
            PATCH_FILE_PREFIX.concat(
                "@@\n"
                    + "-foo-origin\n"
                    + "\\ No newline at end of file\n"
                    + "+foo-destination\n"
                    + "\\ No newline at end of file\n"));
    assertThat(
            Files.readString(
                root.resolve(SOME_DIR).resolve("file2.txt".concat(PATCH_FILE_NAME_SUFFIX))))
        .isEqualTo(
            PATCH_FILE_PREFIX.concat(
                "@@\n"
                    + "-bar-origin\n"
                    + "\\ No newline at end of file\n"
                    + "+bar-destination\n"
                    + "\\ No newline at end of file\n"));
    assertThat(
            Files.readString(
                root.resolve(SOME_DIR).resolve("file3.java".concat(PATCH_FILE_NAME_SUFFIX))))
        .isEqualTo(
            PATCH_FILE_PREFIX.concat(
                "@@\n"
                    + "-public static foo() {\n"
                    + "-  System.out.println(\"foo\");\n"
                    + "+public static bar() {\n"
                    + "+  System.out.println(\"bar\");\n"
                    + " }\n"
                    + " \n"
                    + " \n"
                    + "@@ public static common() {  System.out.println(\"common\");\n"
                    + " \n"
                    + " \n"
                    + " \n"
                    + "-public static fooAgain() {\n"
                    + "-  System.out.println(\"foo again\");}\n"
                    + "\\ No newline at end of file\n"
                    + "+public static barAgain() {\n"
                    + "+  System.out.println(\"bar again\");}\n"
                    + "\\ No newline at end of file\n"));
  }

  @Test
  public void directoryPrefixNoTrailingSlash_patchFilesGeneratedAndWritten()
      throws Exception {
    String trailingSlash = "some/dir";
    writeFile(origin, trailingSlash.concat("/file1.txt"), "foo-origin");
    writeFile(destination, trailingSlash.concat("/file1.txt"), "foo-destination");

    AutoPatchUtil.generatePatchFiles(
        origin,
        destination,
        Path.of(trailingSlash),
        null,
        VERBOSE,
        testEnv,
        PATCH_FILE_PREFIX,
        PATCH_FILE_NAME_SUFFIX,
        root,
        true,
        true,
        Glob.ALL_FILES);

    assertThat(
            Files.readString(
                root.resolve(trailingSlash).resolve("file1.txt".concat(PATCH_FILE_NAME_SUFFIX))))
        .isEqualTo(
            PATCH_FILE_PREFIX.concat(
                "@@\n"
                    + "-foo-origin\n"
                    + "\\ No newline at end of file\n"
                    + "+foo-destination\n"
                    + "\\ No newline at end of file\n"));
  }

  @Test
  public void emptyDiffGeneratesNoPatchFiles() throws Exception {
    writeFile(origin, SOME_DIR.concat("file1.txt"), "foo");
    writeFile(origin, SOME_DIR.concat("b/file2.txt"), "bar");
    writeFile(destination, SOME_DIR.concat("file1.txt"), "foo");
    writeFile(destination, SOME_DIR.concat("b/file2.txt"), "bar");

    AutoPatchUtil.generatePatchFiles(
        origin,
        destination,
        Path.of(SOME_DIR),
        null,
        VERBOSE,
        testEnv,
        PATCH_FILE_PREFIX,
        PATCH_FILE_NAME_SUFFIX,
        root,
        true,
        true,
        Glob.ALL_FILES);

    assertThat(
            Files.exists(
                root.resolve(SOME_DIR).resolve("file1.txt".concat(PATCH_FILE_NAME_SUFFIX))))
        .isFalse();
    assertThat(
            Files.exists(
                root.resolve(SOME_DIR).resolve("b/file2.txt".concat(PATCH_FILE_NAME_SUFFIX))))
        .isFalse();
  }

  @Test
  public void nullPatchFilePrefixAndDirectory() throws Exception {
    writeFile(origin, SOME_DIR.concat("file1.txt"), "foo");
    writeFile(origin, SOME_DIR.concat("b/file2.txt"), "bar");
    writeFile(destination, SOME_DIR.concat("file1.txt"), "foo2");
    writeFile(destination, SOME_DIR.concat("b/file2.txt"), "bar2");

    AutoPatchUtil.generatePatchFiles(
        origin,
        destination,
        Path.of(SOME_DIR),
        null,
        VERBOSE,
        testEnv,
        null,
        PATCH_FILE_NAME_SUFFIX,
        root,
        true,
        true,
        Glob.ALL_FILES);

    assertThat(
            Files.exists(
                root.resolve(SOME_DIR).resolve("/file1.txt".concat(PATCH_FILE_NAME_SUFFIX))))
        .isFalse();
    assertThat(
            Files.exists(
                root.resolve(SOME_DIR).resolve("/b/file2.txt".concat(PATCH_FILE_NAME_SUFFIX))))
        .isFalse();
  }

  @Test
  public void noDiffPatchFileDeleted() throws Exception {
    writeFile(origin, SOME_DIR.concat("file1.txt"), "foo");
    writeFile(destination, SOME_DIR.concat("file1.txt"), "foo");
    writeFile(
        root,
        SOME_DIR.concat("file1.txt").concat(PATCH_FILE_NAME_SUFFIX),
        PATCH_FILE_PREFIX.concat(
            "@@\n"
                + "-foo-origin\n"
                + "\\ No newline at end of file\n"
                + "+foo-destination\n"
                + "\\ No newline at end of file\n"));

    assertThat(
            Files.exists(root.resolve(SOME_DIR.concat("file1.txt").concat(PATCH_FILE_NAME_SUFFIX))))
        .isTrue();
    AutoPatchUtil.generatePatchFiles(
        origin,
        destination,
        Path.of(SOME_DIR),
        null,
        VERBOSE,
        testEnv,
        null,
        PATCH_FILE_NAME_SUFFIX,
        root,
        true,
        true,
        Glob.ALL_FILES);

    assertThat(
            Files.exists(root.resolve(SOME_DIR.concat("file1.txt").concat(PATCH_FILE_NAME_SUFFIX))))
        .isFalse();
  }

  @Test
  public void noOriginFileLeadsToPatchFileDeletion() throws Exception {
    writeFile(destination, SOME_DIR.concat("file1.txt"), "foo");
    writeFile(
        destination,
        SOME_DIR.concat("file1.txt").concat(PATCH_FILE_NAME_SUFFIX),
        PATCH_FILE_PREFIX.concat(
            "@@\n"
                + "-foo-origin\n"
                + "\\ No newline at end of file\n"
                + "+foo-destination\n"
                + "\\ No newline at end of file\n"));
    writeFile(
        root,
        SOME_DIR.concat("file1.txt").concat(PATCH_FILE_NAME_SUFFIX),
        PATCH_FILE_PREFIX.concat(
            "@@\n"
                + "-foo-origin\n"
                + "\\ No newline at end of file\n"
                + "+foo-destination\n"
                + "\\ No newline at end of file\n"));

    assertThat(
            Files.exists(root.resolve(SOME_DIR.concat("file1.txt").concat(PATCH_FILE_NAME_SUFFIX))))
        .isTrue();
    AutoPatchUtil.generatePatchFiles(
        origin,
        destination,
        Path.of(SOME_DIR),
        null,
        VERBOSE,
        testEnv,
        null,
        PATCH_FILE_NAME_SUFFIX,
        root,
        true,
        true,
        Glob.ALL_FILES);

    assertThat(
            Files.exists(root.resolve(SOME_DIR.concat("file1.txt").concat(PATCH_FILE_NAME_SUFFIX))))
        .isFalse();
  }

  @Test
  public void crAtEolDiff() throws Exception {
    writeFile(origin, SOME_DIR.concat("file1.txt"), "foo\r\n");
    writeFile(destination, SOME_DIR.concat("file1.txt"), "foo\n");

    AutoPatchUtil.generatePatchFiles(
        origin,
        destination,
        Path.of(SOME_DIR),
        null,
        VERBOSE,
        testEnv,
        "I'm a file prefix",
        PATCH_FILE_NAME_SUFFIX,
        root,
        true,
        true,
        Glob.ALL_FILES);

    assertThat(
            Files.exists(
                root.resolve(SOME_DIR).resolve("file1.txt".concat(PATCH_FILE_NAME_SUFFIX))))
        .isFalse();
  }

  @Test
  public void stripFileNamesButNotLineNumbers() throws Exception {
    writeFile(origin, SOME_DIR.concat("file1.txt"), "foo-origin");
    writeFile(destination, SOME_DIR.concat("file1.txt"), "foo-destination");

    AutoPatchUtil.generatePatchFiles(
        origin,
        destination,
        Path.of(SOME_DIR),
        null,
        VERBOSE,
        testEnv,
        PATCH_FILE_PREFIX,
        PATCH_FILE_NAME_SUFFIX,
        root,
        true,
        false,
        Glob.ALL_FILES);

    assertThat(
            Files.readString(
                root.resolve(SOME_DIR).resolve("file1.txt".concat(PATCH_FILE_NAME_SUFFIX))))
        .isEqualTo(
            PATCH_FILE_PREFIX.concat(
                "@@ -1 +1 @@\n"
                    + "-foo-origin\n"
                    + "\\ No newline at end of file\n"
                    + "+foo-destination\n"
                    + "\\ No newline at end of file\n"));
  }

  @Test
  public void stripLineNumbersButNotFileNames() throws Exception {
    writeFile(origin, SOME_DIR.concat("file1.txt"), "foo-origin");
    writeFile(destination, SOME_DIR.concat("file1.txt"), "foo-destination");

    AutoPatchUtil.generatePatchFiles(
        origin,
        destination,
        Path.of(SOME_DIR),
        null,
        VERBOSE,
        testEnv,
        PATCH_FILE_PREFIX,
        PATCH_FILE_NAME_SUFFIX,
        root,
        false,
        true,
        Glob.ALL_FILES);

    assertThat(
            Files.readString(
                root.resolve(SOME_DIR).resolve("file1.txt".concat(PATCH_FILE_NAME_SUFFIX))))
        .isEqualTo(
            PATCH_FILE_PREFIX.concat(
                "diff --git a/origin/some/dir/file1.txt b/destination/some/dir/file1.txt\n"
                    + "index 8d53fda..64ace07 100644\n"
                    + "--- a/origin/some/dir/file1.txt\n"
                    + "+++ b/destination/some/dir/file1.txt\n"
                    + "@@\n"
                    + "-foo-origin\n"
                    + "\\ No newline at end of file\n"
                    + "+foo-destination\n"
                    + "\\ No newline at end of file\n"));
  }

  private Path createDir(Path parent, String name) throws IOException {
    Path path = parent.resolve(name);
    Files.createDirectories(path);
    return path;
  }

  private void writeFile(Path parent, String fileName, String fileContents) throws IOException {
    Path filePath = parent.resolve(fileName);
    Files.createDirectories(filePath.getParent());
    Files.writeString(parent.resolve(filePath), fileContents);
  }
}
